/**
 * Copyright © 2008, EMC Corporation.
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 *     + Redistributions of source code must retain the above copyright notice, 
 *       this list of conditions and the following disclaimer.
 *     + Redistributions in binary form must reproduce the above copyright 
 *       notice, this list of conditions and the following disclaimer in the 
 *       documentation and/or other materials provided with the distribution.
 *     + The name of EMC Corporation may not be used to endorse or promote 
 *       products derived from this software without specific prior written 
 *       permission.
 *
 *      THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 *      "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 *      TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 *      PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
 *      BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 *      CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 *      SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 *      INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *      CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 *      ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 *      POSSIBILITY OF SUCH DAMAGE.
 */
package com.emc.esu.api.rest {
	import com.adobe.net.URI;
	import com.emc.esu.api.Acl;
	import com.emc.esu.api.Checksum;
	import com.emc.esu.api.DirectoryEntry;
	import com.emc.esu.api.EsuApi;
	import com.emc.esu.api.EsuError;
	import com.emc.esu.api.Extent;
	import com.emc.esu.api.Grant;
	import com.emc.esu.api.Grantee;
	import com.emc.esu.api.Identifier;
	import com.emc.esu.api.ListOptions;
	import com.emc.esu.api.Metadata;
	import com.emc.esu.api.MetadataList;
	import com.emc.esu.api.MetadataTag;
	import com.emc.esu.api.MetadataTags;
	import com.emc.esu.api.ObjectExpiration;
	import com.emc.esu.api.ObjectId;
	import com.emc.esu.api.ObjectInformation;
	import com.emc.esu.api.ObjectMetadata;
	import com.emc.esu.api.ObjectPath;
	import com.emc.esu.api.ObjectReplica;
	import com.emc.esu.api.ObjectRetention;
	import com.emc.esu.api.Permission;
	import com.emc.esu.api.ServiceInformation;
	
	import flash.utils.ByteArray;
	
	import mx.logging.ILogger;
	import mx.logging.Log;
	import mx.utils.Base64Decoder;
	import mx.utils.StringUtil;

	/**
	 * Implements the REST version of the ESU API.
	 */
	public class EsuRestApi implements EsuApi {
		private static const OBJECTID_EXTRACTOR:RegExp = /\/[0-9a-zA-Z]+\/objects\/([0-9a-f]{44})/;
		private static var cosNs:Namespace = new Namespace( "http://www.emc.com/cos/" );
		

	    private var host:String;
	    private var port:int;
	    private var uid:String;
	    private var secret:ByteArray;
	
	    private var context:String = "/rest";
	    private var proto:String;
	    
	    private var _connectTimeout:int = 30000; // ms
	    
	    private var logger:ILogger;
	    
	    public function set connectTimeout( val:int ):void {
	    	this._connectTimeout = val;
	    } 
	    public function get connectTimeout():int {
	    	return this._connectTimeout;
	    }

		
		/**
		 * Creates a new connector to the ESU REST API
		 * @param host the hostname or IP address of the ESU access node.
		 * @param port the port number of the ESU server (usually 80 for HTTP or 443 for HTTPS).
		 * @param uid the UID to use for connections
		 * @param sharedSecret the shared secret for the UID.
		 */
		public function EsuRestApi( host:String, port:int, uid:String, sharedSecret:String ) {
			this.host = host;
			this.port = port;
			this.uid = uid;
			
			this.logger = Log.getLogger("EsuApi");
			
			var decoder:Base64Decoder = new Base64Decoder();
			decoder.decode( sharedSecret );
			this.secret = decoder.drain();
			
			if( port == 443 ) {
	            proto = "https";
	        } else {
	            proto = "http";
	        }

		}
		
		/**
	     * Creates a new object in the cloud.
	     * @param acl Access control list for the new object.  May be null
	     * to use a default ACL
	     * @param metadata Metadata for the new object.  May be null for
	     * no metadata.
	     * @param data The initial contents of the object.  May be appended
	     * to later.  May be null to create an object with no content.
	     * @param mimeType the MIME type of the content.  Optional, 
	     * may be null.  If data is non-null and mimeType is null, the MIME
	     * type will default to application/octet-stream.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     * @param checksum if not null, use the Checksum object to compute
	     * the checksum for the create object request.  If appending
	     * to the object with subsequent requests, use the same
	     * checksum object for each request.
	     */
		public function createObject(acl:Acl, metadata:MetadataList, data:ByteArray, 
			mimeType:String, callback:Function, state:Object, checksum:Checksum = null):void {
				
            var resource:String = context + "/objects";
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "POST" );
            request.setSecret( secret );

            // Figure out the mimetype
            if( mimeType == null ) {
                mimeType = "application/octet-stream";
            }

            request.addHeader( "Content-Type", mimeType );
            request.addHeader( "x-emc-uid", uid );

            // Process metadata
            if( metadata != null ) {
                processMetadata( metadata, request );
            }

            // Add acl
            if( acl != null ) {
                processAcl( acl, request );
            }

            // Process data
            if( data == null ) {
                data = new ByteArray();
            }
            request.setRequestData( data );
            
            if( checksum != null ) {
            	// Update the hash
            	var pos:int = data.position;
            	checksum.update(data);
            	data.position = pos;
            	request.addHeader( "x-emc-wschecksum", checksum.toString() );
            }

			// Set the response processor
            request.setResponseProcessor( processObjectIdResponse );

			// Execute the request
			request.send();
		}
		
		/**
	     * Creates a new object in the cloud on the given path.  The
	     * ID of the new object will be returned through the callback.
	     * @param path ObjectPath the path to create the new object on.
	     * @param acl Access control list for the new object.  May be null
	     * to use a default ACL
	     * @param metadata Metadata for the new object.  May be null for
	     * no metadata.
	     * @param data The initial contents of the object.  May be appended
	     * to later.  May be null to create an object with no content.
	     * @param mimeType the MIME type of the content.  Optional, 
	     * may be null.  If data is non-null and mimeType is null, the MIME
	     * type will default to application/octet-stream.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     * @param checksum if not null, use the Checksum object to compute
	     * the checksum for the create object request.  If appending
	     * to the object with subsequent requests, use the same
	     * checksum object for each request.
	     */
	    public function createObjectOnPath( path:ObjectPath, acl:Acl, metadata:MetadataList, 
	            data:ByteArray, mimeType:String, callback:Function, state:Object, checksum:Checksum = null ):void {
            var resource:String = getResourcePath( context, path );
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "POST" );
            request.setSecret( secret );

            // Figure out the mimetype
            if( mimeType == null ) {
                mimeType = "application/octet-stream";
            }

            request.addHeader( "Content-Type", mimeType );
            request.addHeader( "x-emc-uid", uid );

            // Process metadata
            if( metadata != null ) {
                processMetadata( metadata, request );
            }

            // Add acl
            if( acl != null ) {
                processAcl( acl, request );
            }

            // Process data
            if( data == null ) {
                data = new ByteArray();
            }
            request.setRequestData( data );

            if( checksum != null ) {
            	// Update the hash
            	var pos:int = data.position;
            	checksum.update(data);
            	data.position = pos;
            	request.addHeader( "x-emc-wschecksum", checksum.toString() );
            }

			// Set the response processor
            request.setResponseProcessor( processObjectIdResponse );

			// Execute the request
			request.send();
		}

		
		/**
		 * Processes responses that contain a single ObjectId.  Extracts the new object
		 * ID from the 'lcoation' header.
		 * @param request the EsuRestRequest to process
		 * @return the ObjectId extracted from the location header.
		 */
		private function processObjectIdResponse( request:EsuRestRequest ):Object {
			// The new object ID is returned in the location response header
            var location:String = request.getReponseHeaders()[ "location" ];
            if( location == null ) {
            	location = request.getReponseHeaders()["Location"];
            }

            // Parse the value out of the URL
            if( OBJECTID_EXTRACTOR.test( location ) ) {
	            var m:Object = OBJECTID_EXTRACTOR.exec( location );
                var id:String = m[1];
                logger.debug( "Id: " + id );
                return new ObjectId( id );
            } else {
                throw new EsuError( "Could not find ObjectId in " + location );
            }

		}
		
	    /**
	     * Updates an object in the cloud.
	     * @param id The ID of the object to update
	     * @param acl Access control list for the new object. Optional, default
	     * is NULL to leave the ACL unchanged.
	     * @param metadata Metadata list for the new object.  Optional,
	     * default is NULL for no changes to the metadata.
	     * @param data The new contents of the object.  May be appended
	     * to later. Optional, default is NULL (no content changes).
	     * @param extent portion of the object to update.  May be null to indicate
	     * the whole object is to be replaced.  If not null, the extent size must
	     * match the data size.
	     * @param mimeType the MIME type of the content.  Optional, 
	     * may be null.  If data is non-null and mimeType is null, the MIME
	     * type will default to application/octet-stream.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function updateObject( id:Identifier, acl:Acl, metadata:MetadataList, extent:Extent, data:ByteArray, 
			mimeType:String, callback:Function, state:Object, checksum:Checksum = null ):void {
				
            var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "PUT" );
            request.setSecret( secret );

            // Figure out the mimetype
            if( mimeType == null ) {
                mimeType = "application/octet-stream";
            }

            request.addHeader( "Content-Type", mimeType );
            request.addHeader( "x-emc-uid", uid );

            // Process metadata
            if( metadata != null ) {
                processMetadata( metadata, request );
            }

            // Add acl
            if( acl != null ) {
                processAcl( acl, request );
            }
            
             //Add extent if needed
            if( extent != null && !extent.equals( Extent.ALL_CONTENT ) ) {
                var end:Number = extent.getOffset() + ( extent.getSize() - 1 );
                request.addHeader( "Range", "Bytes=" + extent.getOffset() + "-" + end ); 
            }

            // Process data
            if( data == null ) {
                data = new ByteArray();
            }
            request.setRequestData( data );
            
            if( checksum != null ) {
            	// Update the hash
            	var pos:int = data.position;
            	checksum.update(data);
            	data.position = pos;
            	request.addHeader( "x-emc-wschecksum", checksum.toString() );
            }


			// Execute the request
			request.send();
			
		}
		
	    /**
	     * Writes the metadata into the object. If the tag does not exist, it is 
	     * created and set to the corresponding value. If the tag exists, the 
	     * existing value is replaced.
	     * @param id the identifier of the object to update
	     * @param metadata metadata to write to the object.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function setUserMetadata(id:Identifier, metadata:MetadataList, callback:Function, state:Object):void {
            var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "metadata/user"  );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "POST" );
            request.setSecret( secret );

            request.addHeader( "x-emc-uid", uid );

            // Process metadata
            if( metadata != null ) {
                processMetadata( metadata, request );
            }

			// Execute the request
			request.send();
		}
		
	    /**
	     * Sets (overwrites) the ACL on the object.
	     * @param id the identifier of the object to change the ACL on.
	     * @param acl the new ACL for the object.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function setAcl(id:Identifier, acl:Acl, callback:Function, state:Object):void {
            var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "acl" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "POST" );
            request.setSecret( secret );

            request.addHeader( "x-emc-uid", uid );

            // Process acl
            if( acl != null ) {
                processAcl( acl, request );
            }

			// Execute the request
			request.send();
			
		}
		
	    /**
	     * Deletes an object from the cloud.
	     * @param id the identifier of the object to delete.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function deleteObject(id:Identifier, callback:Function, state:Object):void {
            var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "DELETE" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Execute the request
			request.send();
		}
		
	    /**
	     * Fetches the user metadata for the object.
	     * @param id the identifier of the object whose user metadata
	     * to fetch.
	     * @param tags A list of user metadata tags to fetch.  Optional.  If null,
	     * all user metadata will be fetched.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function getUserMetadata(id:Identifier, tags:MetadataTags, callback:Function, state:Object):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "metadata/user");
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            if( tags != null ) {
            	processTags( tags, request );
            }

            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processGetMetadataResponse );

			// Execute the request
			request.send();
		}
		
		/**
		 * Processes responses that contain user metadata.  Regular metadata is
		 * extracted from the x-emc-meta header and listable metadata is extracted
		 * from the x-emc-listable-meta header.
		 * @param request the EsuRestRequest to process
		 * @return a MetadataList containing the extracted metadata
		 */
		private function processGetMetadataResponse( request:EsuRestRequest ):Object {
			// Parse return headers.  Regular metadata is in x-emc-meta and
            // listable metadata is in x-emc-listable-meta
            var meta:MetadataList = new MetadataList();
            readMetadata( meta, request.getReponseHeaders()[ "x-emc-meta" ], false );           
            readMetadata( meta, request.getReponseHeaders()[ "x-emc-listable-meta" ], true );
            
            return meta;
		}
		
	    /**
	     * Fetches the system metadata for the object.
	     * @param id the identifier of the object whose system metadata
	     * to fetch.
	     * @param tags A list of system metadata tags to fetch.  Optional.
	     * Default value is null to fetch all system metadata.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function getSystemMetadata(id:Identifier, tags:MetadataTags, callback:Function, state:Object):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "metadata/system");
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            if( tags != null ) {
            	processTags( tags, request );
            }

            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processGetMetadataResponse );

			// Execute the request
			request.send();
		}
				
	    /**
	     * Reads an object's content.
	     * @param id the identifier of the object whose content to read.
	     * @param extent the portion of the object data to read.  Optional.
	     * Default is null to read the entire object.
	     * @param buffer the buffer to use to read the extent.  Must be large
	     * enough to read the response or an error will be thrown.  If null,
	     * a buffer will be allocated to hold the response data.  If you pass
	     * a buffer that is larger than the extent, only extent.getSize() bytes
	     * will be valid.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function readObject(id:Identifier, extent:Extent, buffer:ByteArray, 
			callback:Function, state:Object, checksum:Checksum = null):void {
				
            var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            request.setPrivateState( checksum );
            
             //Add extent if needed
            if( extent != null && !extent.equals( Extent.ALL_CONTENT ) ) {
                var end:Number = extent.getOffset() + ( extent.getSize() - 1 );
                request.addHeader( "Range", "Bytes=" + extent.getOffset() + "-" + end ); 
            }
            
            // If a buffer was included, pass it
            if( buffer != null ) {
            	request.setResponseBuffer( buffer );
            }

            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processReadObjectResponse );

			// Execute the request
			request.send();
		}
		
		/**
	     * Renames a file or directory within the namespace.
	     * @param source The file or directory to rename
	     * @param destination The new path for the file or directory
	     * @param force If true, the desination file or 
	     * directory will be overwritten.  Directories must be empty to be 
	     * overwritten.  Also note that overwrite operations on files are
	     * not synchronous; a delay may be required before the object is
	     * available at its destination.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
	    public function rename(source:ObjectPath, destination:ObjectPath, force:Boolean, callback:Function, state:Object):void {
			var resource:String = getResourcePath( context, source );
            var uri:URI = buildUri( resource, "rename" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "POST" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );
            
            var destPath:String = destination.toString();
            if (destPath.charAt(0) == '/')
            {
                destPath = destPath.substr( 1 );
            }
            request.addHeader("x-emc-path", destPath);

            if (force) {
                request.addHeader("x-emc-force", "true");
            }

			// Execute the request
			request.send();
	    }
	    
	    /**
	     * Gets the current Atmos server information.  Currently, this simply
	     * returns the version of Atmos that is running.
	     * @return the ServiceInformation object
	     */
	    public function getServiceInformation(callback:Function, state:Object):void {
			var resource:String = context + "/service";
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processServiceInformationResponse );

			// Execute the request
			request.send();
	    }


		
		/**
		 * Processes object data returned from readObject.  If the Transfer-Encoding
		 * header is set to "chunked", the response data will be post-processed to
		 * extract the chunked data.
		 * @param request the EsuRestRequest to process
		 * @return a ByteArray containing the object data
		 */
		private function processReadObjectResponse( request:EsuRestRequest ):Object {
			var data:ByteArray = request.getReponseData();
			logger.debug( "processReadObjectResponse: " + data.length + " bytes" );
			data.position = 0;
			var checksum:Checksum = Checksum(request.getPrivateState());
			
			var headers:Object = request.getReponseHeaders();
			
			logger.debug( "Transfer encoding: " + headers["Transfer-Encoding"] );
			if( request.getReponseHeaders()["Transfer-Encoding"] != null &&
				request.getReponseHeaders()["Transfer-Encoding"] == "chunked" ) {
				logger.debug( "Processing chunked response" );
			
				// The response has been chunked.  We need to reparse the body
				// to remove the chunk boundaries.		
				var data2:ByteArray = new ByteArray();
				while( true ) {
					var chunkSize:int = readChunkSize( data );
					if( chunkSize == 0 ) {
						break;
					}
					data2.writeBytes( data, data.position, chunkSize );
					data.position += chunkSize;
					// skip CR-LF at end of chunk
					data.readUTFBytes(2);
				}
				
				if( checksum != null ) {
					checksum.setExpectedValue( headers["X-Emc-Wschecksum"] );
					checksum.update( data2 );
					data2.position = 0;
				}
				return data2;
			}
			
			if( checksum != null ) {
				checksum.setExpectedValue( headers["x-emc-wschecksum"] );
				checksum.update( data );
				data.position = 0;
			}
			return data;
		}
		
		/**
		 * Extracts a chunk size from the byte array's stream.  Each
		 * chunk size will be a hex number followed by \r\n.
		 * @param ba the ByteArray to read from
		 * @return the chunk size in bytes.
		 */
		private function readChunkSize( ba:ByteArray ):Number {
			var sizeHex:String = "0x";
			while( true ) {
				if( ba[ba.position] != 13 && ba[ba.position+1] != 10 ) {
					sizeHex += ba.readUTFBytes( 1 );
				} else {
					// found CRLF
					ba.position += 2;
					var sizeNum:Number = Number( sizeHex );
					logger.debug( "Chunk size: " + sizeHex + " (" + sizeNum + ")" );
					return sizeNum;
				}
			}
			return -1;
		}
		
	    /**
	     * Returns an object's ACL
	     * @param id the identifier of the object whose ACL to read
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function getAcl(id:Identifier, callback:Function, state:Object):void {
			var resource:String = getResourcePath( context, id ) ;
            var uri:URI = buildUri( resource, "acl" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processGetAclResponse );

			// Execute the request
			request.send();
		}
		
		/**
		 * Reads ACL data from the x-emc-useracl and x-emc-groupacl headers.
		 * @param request the EsuRestRequest to process
		 * @return an Acl object extracted from the response
		 */
		private function processGetAclResponse( request:EsuRestRequest ):Object {
			// Parse return headers.  User grants are in x-emc-useracl and
            // group grants are in x-emc-groupacl
            var acl:Acl = new Acl();
            readAcl( acl, request.getReponseHeaders()[ "x-emc-useracl" ], Grantee.GRANT_TYPE_USER );              
            readAcl( acl, request.getReponseHeaders()[ "x-emc-groupacl" ], Grantee.GRANT_TYPE_GROUP );
            
            return acl;
		}
		
	    /**
	     * Deletes metadata items from an object.
	     * @param id the identifier of the object whose metadata to 
	     * delete.
	     * @param tags the list of metadata tags to delete.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function deleteUserMetadata(id:Identifier, tags:MetadataTags, callback:Function, state:Object):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "metadata/user" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "DELETE" );
            request.setSecret( secret );
            
            if( tags != null ) {
            	processTags( tags, request );
            } else {
            	throw new EsuError( "Must specify tags to delete" );
            }

            request.addHeader( "x-emc-uid", uid );

			// Execute the request
			request.send();

		}
		
	    /**
	     * Lists the versions of an object.
	     * @param id the object whose versions to list.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function listVersions(id:Identifier, callback:Function, state:Object):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "versions" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processVersionListResponse );

			// Execute the request
			request.send();
		}
		
		/**
		 * Reads a list of objects from the response body
		 * @param request the EsuRestRequest to process
		 * @return an Array of ObjectIds extracted from the body
		 */
		private function processObjectListResponse( request:EsuRestRequest ):Object {
			var data:ByteArray = request.getReponseData();
			data.position = 0;
			var dataString:String = data.readUTFBytes( data.length );
			var results:Array = parseObjectList( dataString );
			return results;
		}
		
		/**
		 * Reads a list of objects from the response body
		 * @param request the EsuRestRequest to process
		 * @return an Array of ObjectIds extracted from the body
		 */
		private function processVersionListResponse( request:EsuRestRequest ):Object {
			var data:ByteArray = request.getReponseData();
			data.position = 0;
			var dataString:String = data.readUTFBytes( data.length );
			var results:Array = parseVersionList( dataString );
			return results;
		}
		
		/**
		 * Reads a list of objects from the response body
		 * @param request the EsuRestRequest to process
		 * @return an Array of ObjectIds extracted from the body
		 */
		private function processObjectMetaListResponse( request:EsuRestRequest ):Object {
			var data:ByteArray = request.getReponseData();
			data.position = 0;
			var dataString:String = data.readUTFBytes( data.length );
			var results:Array = parseObjectMetaList( dataString );
			return results;
		}
		
	    /**
	     * Creates a new immutable version of an object.
	     * @param id the object to version
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function versionObject(id:Identifier, callback:Function, state:Object):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "versions" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "POST" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processObjectIdResponse );

			// Execute the request
			request.send();
			
		}
		
	    /**
	     * Deletes a version
	     * @param id the version object to delete
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
	    public function deleteVersion( id:ObjectId, callback:Function, state:Object ):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "versions" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "DELETE" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Execute the request
			request.send();
	    }
	    
	    /**
	     * Restores a version of an object to the base revision (i.e. "promotes" a
	     * version to the current version)
	     * @param id the base object
	     * @param vId the version ID to promote
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
	    public function restoreVersion( id:Identifier, vId:ObjectId, callback:Function, state:Object ):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "versions" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "PUT" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );
            request.addHeader( "x-emc-version-oid", vId.toString() );

			// Execute the request
			request.send();
	    	
	    }


		/**
	     * Lists all objects with the given tag.  Returns the IDs and metadata
	     * of all objects matching the tag.
	     * @param tag the tag to search for.  May be a MetadataTag object
	     * or a String containing the tag name.
	     * @return An array of Objects.  Each object will have a property
	     * named 'id' containing the ObjectId and a property named 'meta'
	     * containing the ObjectMetadata.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function listObjectsWithMetadata(tag:Object, 
			callback:Function, state:Object):void {
			
			var tagName:String = "";
			if( tag is MetadataTag ) {
				tagName = MetadataTag( tag ).getName();
			} else {
				tagName = tag.toString();
			}
			
			var resource:String = context + "/objects";
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            
            // Add tag
            if( tagName != null ) {
               	request.addHeader( "x-emc-tags", tagName );
            } else {
                throw new EsuError( "Tag cannot be null" );
            }
            
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );
            request.addHeader( "x-emc-include-meta", "1" );

			// Set the response processor
            request.setResponseProcessor( processObjectMetaListResponse );

			// Execute the request
			request.send();
		}

				
	    /**
	     * Lists all objects with the given tag.
	     * @param tag the tag to search for.  May be a MetadataTag object
	     * or a String containing the tag name.
		 * @param options options for returning the list
	     * @return The list of objects with the given tag.  If no objects
	     * are found the array will be empty.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function listObjects(tag:Object, options:ListOptions, callback:Function, state:Object):void {
			var tagName:String = "";
			if( tag is MetadataTag ) {
				tagName = MetadataTag( tag ).getName();
			} else {
				tagName = tag.toString();
			}
			
			var resource:String = context + "/objects";
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            
            // Add tag
            if( tagName != null ) {
               	request.addHeader( "x-emc-tags", tagName );
            } else {
                throw new EsuError( "Tag cannot be null" );
            }
            
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processObjectListResponse );

			// Execute the request
			request.send();
			
		}
		
	    /**
	     * Returns a list of the tags that are listable the current user's tennant.
	     * @param tag optional.  If specified, the list will be limited to the tags
	     * under the specified tag.  If null, only top level tags will be returned.
	     * May be a MetadataTag object or a String containing the tag name.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function getListableTags(tag:Object, callback:Function, state:Object):void {
			var tagName:String = null;
			if( tag is MetadataTag ) {
				tagName = MetadataTag( tag ).getName();
			} else if( tag != null ) {
				tagName = tag.toString();
			}
			
			var resource:String = context + "/objects";
            var uri:URI = buildUri( resource, "listabletags" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            
            // Add tag
            if( tagName != null ) {
               	request.addHeader( "x-emc-tags", tagName );
            }
            
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processTagsResponse );

			// Execute the request
			request.send();
		}
		
		/**
		 * Processes a response that contains metadata tags.  Regular tags will be
		 * read from the x-emc-tags header and listable tags will be read from the
		 * x-emc-listable-tags header.
		 * @param request the EsuRestRequest to process
		 * @return a MetadataTags object containing the extracted tags.
		 */
		private function processTagsResponse( request:EsuRestRequest ):Object {
			var tags:MetadataTags = new MetadataTags();
			
            readTags( tags, request.getReponseHeaders()[ "x-emc-listable-tags" ], true );
            readTags( tags, request.getReponseHeaders()[ "x-emc-tags" ], false );
            
            return tags;
		}
		
	    /**
	     * Returns the list of user metadata tags assigned to the object.
	     * @param id the object whose metadata tags to list
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function listUserMetadataTags(id:Identifier, callback:Function, state:Object):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "metadata/tags" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processTagsResponse );

			// Execute the request
			request.send();
		}
		
	    /**
	     * Executes a query for objects matching the specified XQuery string.
	     * @param xquery the XQuery string to execute against the cloud.
	     * @return the list of objects matching the query.  If no objects
	     * are found, the array will be empty.
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function queryObjects(xquery:String, callback:Function, state:Object):void {
			var resource:String = context + "/objects";
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            
            // Add tag
            if( xquery != null ) {
               	request.addHeader( "x-emc-xquery", xquery );
            } else {
                throw new EsuError( "Query cannot be null" );
            }
            
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processObjectListResponse );

			// Execute the request
			request.send();
		}
		
		/**
	     * Returns all the metadata for the object: system metadata, user
	     * metadata, and the ACL as a ObjectMetadata object.
	     * @param id the object whose metadata to get
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function getAllMetadata( id:Identifier, callback:Function, state:Object ):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "HEAD" );
            request.setSecret( secret );
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processGetAllMetadataResponse );

			// Execute the request
			request.send();
			
		}
		
		
		/**
		 * Processes responses that contain user metadata.  Regular metadata is
		 * extracted from the x-emc-meta header and listable metadata is extracted
		 * from the x-emc-listable-meta header.
		 * @param request the EsuRestRequest to process
		 * @return a MetadataList containing the extracted metadata
		 */
		private function processGetAllMetadataResponse( request:EsuRestRequest ):Object {
			// Parse return headers.  Regular metadata is in x-emc-meta and
            // listable metadata is in x-emc-listable-meta
            var meta:MetadataList = new MetadataList();
            readMetadata( meta, request.getReponseHeaders()[ "x-emc-meta" ], false );           
            readMetadata( meta, request.getReponseHeaders()[ "x-emc-listable-meta" ], true );
            
            var acl:Acl = new Acl();
            readAcl( acl, request.getReponseHeaders()[ "x-emc-useracl" ], Grantee.GRANT_TYPE_USER );              
            readAcl( acl, request.getReponseHeaders()[ "x-emc-groupacl" ], Grantee.GRANT_TYPE_GROUP );

			var data:ObjectMetadata = new ObjectMetadata();
			data.setMetadata( meta );
			data.setAcl( acl );
			            
            return data;
		}

	    /**
	     * Returns all the objects in the specified directory as an array
	     * of DirectoryEntry objects.
	     * @param id the object whose metadata to get
		 * @param options options for listing the directory
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
	     */
		public function listDirectory( path:ObjectPath, options:ListOptions, callback:Function, state:Object ):void {
			if( !path.isDirectory() ) {
				throw new EsuError( "ObjectPath passed to listDirectory must be a directory" );
			}
			
            var resource:String = getResourcePath( context, path );
            var uri:URI = buildUri( resource );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            if( state == null ) {
            	state = new Object();
            }
            state._listDirectoryParentPath = path;
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
			
			// Process options
			if( options != null ) {
				state._options = options;
				if( options.includeMetadata ) {
					request.addHeader( "x-emc-include-meta", "1" );
					if( options.systemMetadata != null ) {
						request.addHeader( "x-emc-system-tags", 
							options.systemMetadata.join(",") );
					}
					if( options.userMetadata != null ) {
						request.addHeader( "x-emc-user-tags", 
							options.userMetadata.join(",") );            			
					}
				}
				if( options.limit > 0 ) {
					request.addHeader( "x-emc-limit", ""+options.limit );
				}
				if( options.token != null ) {
					request.addHeader( "x-emc-token", options.token );
				}
			}
            
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processListDirectoryResponse );

			// Execute the request
			request.send();
			
		}
		
		
		/**
		 * Reads a list of objects from the response body
		 * @param request the EsuRestRequest to process
		 * @return an Array of ObjectIds extracted from the body
		 */
		private function processListDirectoryResponse( request:EsuRestRequest ):Object {
			var data:ByteArray = request.getReponseData();
			data.position = 0;
			var dataString:String = data.readUTFBytes( data.length );
	        var objs:Array = new Array();
	        var state:Object = request.getUserState();
	        var dir:ObjectPath = ObjectPath( state._listDirectoryParentPath );
			var options:ListOptions = ListOptions( state._options );
	        delete state._listDirectoryParentPath;
			delete state._options;
	        
	        var d:XML = new XML( dataString );
	        
	        var children:XMLList = d..cosNs::DirectoryEntry;
	        for each( var child:XML in children ) {
	        	var fileName:String = child.cosNs::Filename.toString();
	        	var fileType:String = child.cosNs::FileType.toString();
	        	var objectId:String = child.cosNs::ObjectID.toString();
				
	        	
	        	fileName = dir.toString() + fileName;
	        	if( fileType == "directory" ) {
	        		// Dirs end in '/'
	        		fileName += "/";
	        	}
	        	
	        	var entry:DirectoryEntry = new DirectoryEntry();
	        	entry.setId( new ObjectId( objectId ) );
	        	entry.setPath( new ObjectPath( fileName ) );
	        	entry.setType( fileType );
	        	objs.push( entry );
	        }

	        return objs;
		}


		/**
		 * Returns the replica information for the object.
	     * @param id the object whose metadata to get
	     * @param callback a callback function with the signature 
	     * <code>function x( state:EsuCallbackState );</code>
	     * @param state an optional user-defined state object to be passed to
	     * the callback method through the EsuCallbackState object.
		 */
		public function getObjectInfo( id:Identifier, callback:Function, state:Object ):void {
			var resource:String = getResourcePath( context, id );
            var uri:URI = buildUri( resource, "info" );
            
            var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
            request.setUserState( state );
            request.setCallback( callback );
            request.setMethod( "GET" );
            request.setSecret( secret );
                       
            request.addHeader( "x-emc-uid", uid );

			// Set the response processor
            request.setResponseProcessor( processGetObjectInfoResponse );

			// Execute the request
			request.send();
		}
		
		/**
		 * Returns the replica information for the object.
		 * @param id the object whose metadata to get
		 * @param callback a callback function with the signature 
		 * <code>function x( state:EsuCallbackState );</code>
		 * @param state an optional user-defined state object to be passed to
		 * the callback method through the EsuCallbackState object.
		 */
		public function getObjectLso( id:Identifier, callback:Function, state:Object ):void {
			var resource:String = getResourcePath( context, id );
			var uri:URI = buildUri( resource, "metadata/user" );
			
			var request:EsuRestRequest = new EsuRestRequest( resource, uri, connectTimeout );
			request.setUserState( state );
			request.setCallback( callback );
			request.setMethod( "GET" );
			request.setSecret( secret );
			
			var tags:MetadataTags = new MetadataTags();
			tags.addTag( new MetadataTag( "user.maui.lso", true ) );
			processTags( tags, request );
			
			request.addHeader( "x-emc-uid", uid );
			
			// Set the response processor
			request.setResponseProcessor( processGetLsoResponse );
			
			// Execute the request
			request.send();
		}


		/**
		 * Changes the protocol used for connections (http or https)
		 */
		public function setProtocol( proto:String ):void {
			this.proto = proto;
		}
		
		/**
		 * Changes the context used for locating the REST services.
		 * By default, this is "/rest".
		 */
		public function setContext( context:String ):void {
			this.context = context;
		}
		
		/**
	     * An Atmos user (UID) can construct a pre-authenticated URL to an 
	     * object, which may then be used by anyone to retrieve the 
	     * object (e.g., through a browser). This allows an Atmos user 
	     * to let a non-Atmos user download a specific object. The 
	     * entire object/file is read.
	     * 
	     * This method is synchronous.  Therefore, no callback is needed.
	     * 
	     * @param id the object to generate the URL for
	     * @param expiration the expiration date of the URL
	     * @return a URL that can be used to share the object's content
	     */
	    public function getShareableUrl( id:Identifier, expiration:Date ):String {
	    	var resource:String = getResourcePath( context, id );
            
            var hashStr:String = "";
            
            var secSinceEpoch:Number = expiration.time / 1000;
            var resString:String = resource.toLowerCase();
            
            hashStr += "GET\n";
            hashStr += resString + "\n";
            hashStr += this.uid + "\n";
            hashStr += secSinceEpoch.toFixed(0);
            
            var signature:String = EsuRestRequest.sign( hashStr, this.secret );
            var query:String = "uid=" + encodeURIComponent( this.uid ) + "&expires=" + secSinceEpoch.toFixed(0) +
                "&signature=" + encodeURIComponent( signature );
            
            var u:String = buildUri( resource ).toString();
            u += "?" + query;
            return u;

	    }
		
		
		/////////////////////
	    // Private Methods //
	    /////////////////////
	
	    /**
	     * Builds a new URL to the given resource
	     */
	    private function buildUri( resource:String, query:String = null):URI {
	    	var uri:URI = new URI();
	    	uri.scheme = proto;
	    	uri.authority = host;
	    	uri.port = ""+port;
	    	uri.path = resource;
	    	if( query != null ) {
	    		uri.query = query;
	    	}

	        return uri;
	    }

		
	    /**
	     * Iterates through the given metadata and adds the appropriate metadata
	     * headers to the request.
	     * @param metadata the metadata to add
	     * @param headers the map of request headers.
	     */
	    private function processMetadata( metadata:MetadataList,
	            request:EsuRestRequest ):void {
	
	        var listable:String = "";
	        var nonListable:String = "";
	
	        logger.debug( "Processing " + metadata + " metadata entries" );
	
	        for( var key:String in metadata.items() ) {
	            var meta:Metadata = metadata.getMetadata( key );
	            if( meta.isListable() ) {
	                if( listable.length > 0 ) {
	                    listable += ", ";
	                }
	                listable += formatTag( meta );
	            } else {
	                if( nonListable.length > 0 ) {
	                    nonListable += ", ";
	                }
	                nonListable += formatTag( meta );
	            }
	        }
	
	        // Only set the headers if there's data
	        if( listable.length > 0 ) {
	            request.addHeader( "x-emc-listable-meta", listable );
	        }
	        if( nonListable.length > 0 ) {
	            request.addHeader( "x-emc-meta", nonListable );
	        }
	
	    }
	
	
	
	    /**
	     * Formats a tag value for passing in the header.
	     */
	    private function formatTag( meta:Metadata ):String {
	        // strip commas and newlines for now.
	        var fixed:String = meta.getValue().replace( /,/g, "" );
	        fixed = fixed.replace( /\n/g, "" );
	        return meta.getName() + "=" + fixed;
	    }
	
	
	    /**
	     * Enumerates the given ACL and creates the appropriate request headers.
	     * @param acl the ACL to enumerate
	     * @param headers the set of request headers.
	     */
	    private function processAcl( acl:Acl, request:EsuRestRequest ):void {
	        var userGrants:String = "";
	        var groupGrants:String = "";
	
	        for each( var g:Object in acl.getGrants() ) {
	            var grant:Grant = Grant(g);
	            if( grant.getGrantee().getType() == Grantee.GRANT_TYPE_USER ) {
	                if( userGrants.length > 0 ) {
	                    userGrants += ",";
	                }
	                userGrants += grant.toString();
	            } else {
	                if( groupGrants.length > 0 ) {
	                    groupGrants += ",";
	                }
	                groupGrants += grant.toString();
	            }
	        }
	
	        request.addHeader( "x-emc-useracl", userGrants );
	        request.addHeader( "x-emc-groupacl",  groupGrants );
	    }
	
	
	    
	    /**
	     * Parses the given header text and appends to the metadata list
	     * @param meta the metadata list to append to
	     * @param header the metadata header to parse
	     * @param listable true if the header being parsed contains listable metadata.
	     */
	    private function readMetadata( meta:MetadataList, header:String, listable:Boolean ):void {
	        if (header == null) {
	            return;
	        }
	
	        var attrs:Array = header.split( "," );
	        for (var i:int = 0; i < attrs.length; i++) {
	            var nvpair:Array = attrs[i].split("=", 2);
	            var name:String = nvpair[0];
	            var value:String = nvpair[1];
	
	            name = StringUtil.trim( name );
	
	            var m:Metadata = new Metadata(name, value, listable);
	            logger.debug("Meta: " + m);
	            meta.addMetadata(m);
	        }
	    }
	
	    /**
	     * Enumerates the given list of metadata tags and sets the x-emc-tags
	     * header.
	     * @param tags the tag list to enumerate
	     * @param headers the HTTP request headers
	     */
	    private function processTags( tags:MetadataTags, request:EsuRestRequest ):void {
	        var taglist:String = "";
	
	        logger.debug("Processing " + tags + " metadata tag entries");
	
	        for( var key:String in tags.items() ) {
	            var tag:MetadataTag = tags.getTag( key );
	            if (taglist.length > 0) {
	                taglist += ",";
	            }
	            taglist += tag.getName();
	        }
	
	        if (taglist.length > 0) {
	            request.addHeader( "x-emc-tags", taglist.toString() );
	        }
	    }
	
	    /**
	     * Parses the value of an ACL response header and builds an ACL
	     * @param acl a reference to the ACL to append to
	     * @param header the acl response header
	     * @param type the type of Grantees in the header (user or group)
	     */
	    private function readAcl( acl:Acl, header:String, type:String ):void {
	        logger.debug("readAcl: " + header);
	        var grants:Array = header.split(",");
	        for (var i:int = 0; i < grants.length; i++) {
	            var nvpair:Array = grants[i].split("=", 2);
	            var grantee:String = nvpair[0];
	            var permission:String = nvpair[1];
	
	            grantee = StringUtil.trim( grantee );
	
	            // Currently, the server returns "FULL" instead of "FULL_CONTROL".
	            // For consistency, change this to value use in the request
	            if ("FULL" == permission ) {
	                permission = Permission.FULL_CONTROL;
	            }
	
	            logger.debug("grant: " + grantee + "." + permission + " (" + type
	                    + ")");
	
	            var ge:Grantee = new Grantee(grantee, type);
	            var gr:Grant = new Grant(ge, permission);
	            logger.debug("Grant: " + gr);
	            acl.addGrant(gr);
	        }
	    }
	
	
	
	    /**
	     * Parses an XML response and extracts the list of ObjectIDs.
	     * @param response the response byte array to parse as XML
	     * @return the list of object IDs contained in the response.
	     */
	    private function parseObjectList( response:String ):Array {
	        var objs:Array = new Array();
	        
	        var d:XML = new XML( response );
	        
	        var children:XMLList = d..cosNs::ObjectID;
	        for each( var child:XML in children ) {
	        	var cstr:String = child.toString();
	        	logger.debug( "Found: " + cstr );
	        	var oid:ObjectId = new ObjectId( cstr );
	        	objs.push( oid );
	        }

	        return objs;
	    }
	    
	    /**
	     * Parses an XML response and extracts the list of ObjectIDs.
	     * @param response the response byte array to parse as XML
	     * @return the list of object IDs contained in the response.
	     */
	    private function parseVersionList( response:String ):Array {
	        var objs:Array = new Array();
	        
	        var d:XML = new XML( response );
	        
	        var children:XMLList = d..cosNs::OID;
	        for each( var child:XML in children ) {
	        	var cstr:String = child.toString();
	        	logger.debug( "Found: " + cstr );
	        	var oid:ObjectId = new ObjectId( cstr );
	        	objs.push( oid );
	        }

	        return objs;
	    }
	    
	    /**
	     * Parses an XML response and extracts the list of ObjectIDs and
	     * metadata.
	     * @param response the response byte array to parse as XML
	     * @return the list of object IDs contained in the response.
	     */
	    private function parseObjectMetaList( response:String ):Array {
	        var objs:Array = new Array();
	        
	        var d:XML = new XML( response );
	        
	        
	        var children:XMLList = d..cosNs::Object;
	        for each( var child:XML in children ) {
	        	var oid:XML = child.cosNs::ObjectID[0];
	        	var cstr:String = oid.toString();
	        	logger.debug( "Found: " + cstr );
	        	var obj:Object = new Object();
	        	obj.id = new ObjectId( cstr );
	        	var mlist:MetadataList = new MetadataList();
	        	obj.meta = mlist;
	        	
	        	var system:XMLList = child..cosNs::SystemMetadataList..cosNs::Metadata;
	        	
	        	for each( var sMeta:XML in system ) {
	        		var sName:String = sMeta.cosNs::Name[0].toString();
	        		var sValue:String = sMeta.cosNs::Value[0].toString();
	        		var sm:Metadata = new Metadata( sName, sValue, false );
	        		mlist.addMetadata( sm );
	        	}
	        	
	        	var user:XMLList = child..cosNs::UserMetadataList..cosNs::Metadata;
	        	for each( var uMeta:XML in user ) {
	        		var uName:String = uMeta.cosNs::Name[0].toString();
	        		var uValue:String = uMeta.cosNs::Value[0].toString();
	        		var uList:Boolean = "true" == uMeta.cosNs::Listable[0].toString();
	        		var um:Metadata = new Metadata( uName, uValue, uList );
	        		mlist.addMetadata( um );
	        	}
	        	
	        	
	        	objs.push( obj );
	        }

	        return objs;
	    }
	    
	    /**
	     * Parses the given header and appends to the list of metadata tags.
	     * @param tags the list of metadata tags to append to
	     * @param header the header to parse
	     * @param listable true if the metadata tags in the header are listable
	     */
	    private function readTags( tags:MetadataTags, header:String, listable:Boolean ):void {
	        if (header == null) {
	            return;
	        }
	
	        var attrs:Array = header.split(",");
	        for (var i:int = 0; i < attrs.length; i++) {
	            var attr:String = StringUtil.trim( attrs[i] )
	            tags.addTag(new MetadataTag(attr, listable));
	        }
	    }
	    
		/**
	     * Gets the appropriate resource path depending on identifier
	     * type.
	     */
	    private function getResourcePath( ctx:String, id:Identifier ):String {
			if( id is ObjectId ) {
				return ctx + "/objects/" + id;
			} else {
				return ctx + "/namespace" + id;
			}
		}

		/**
		 * Processes the replica info response
		 */
		private function processGetObjectInfoResponse( request:EsuRestRequest ):Object {
			var data:ByteArray = request.getReponseData();
			data.position = 0;
			var dataString:String = data.readUTFBytes( data.length );
	        var d:XML = new XML( dataString );
	        
	        var info:ObjectInformation = new ObjectInformation();
	        info.rawXml = dataString;
	        
	        info.objectId = new ObjectId( d.cosNs::objectId.toString() );
	        info.selection = d..cosNs::selection.toString();
			processReplicas( info, d..cosNs::replicas.cosNs::replica );
			processExpiration( info, d..cosNs::expiration );
			processRetention( info, d..cosNs::retention );
			
			return info;
		}
		
		private function processReplicas( info:ObjectInformation, nodes:XMLList ):void {
	        for each( var child:XML in nodes ) {
	        	var replica:ObjectReplica = new ObjectReplica();
	        	
	        	replica.id = child.cosNs::id.toString();
	        	replica.location = child.cosNs::location.toString();
	        	replica.replicaType = child.cosNs::type.toString();
	        	replica.storageType = child.cosNs::storageType.toString();
	        	replica.current = child.cosNs::current.toString() == "true";
	        	
	        	info.replicas.push( replica );
	        }
		}
		
		private function processExpiration( info:ObjectInformation, exp:XMLList ):void {
			info.expiration = new ObjectExpiration();
			
			info.expiration.enabled = exp.cosNs::enabled.toString() == "true";
			if( info.expiration.enabled ) {
				info.expiration.endAt = isoToDate( exp.cosNs::endAt.toString() );
			}
		}

		private function processRetention( info:ObjectInformation, exp:XMLList ):void {
			info.retention = new ObjectRetention();
			
			info.retention.enabled = exp.cosNs::enabled.toString() == "true";
			if( info.retention.enabled ) {
				info.retention.endAt = isoToDate( exp.cosNs::endAt.toString() );
			}
		}

		private function isoToDate(value:String):Date {
			var dateStr:String = value;
			dateStr = dateStr.replace(/\-/g, "/");
			dateStr = dateStr.replace("T", " ");
			dateStr = dateStr.replace("Z", " GMT-0000");
			return new Date(Date.parse(dateStr));
        }
        
        /**
		 * Reads the service information from the response body.
		 * @param request the EsuRestRequest to process
		 * @return a ServiceInformation object
		 */
		private function processServiceInformationResponse( request:EsuRestRequest ):Object {
			var data:ByteArray = request.getReponseData();
			data.position = 0;
			var dataString:String = data.readUTFBytes( data.length );
	        var d:XML = new XML( dataString );
	        var service:ServiceInformation = new ServiceInformation();
	        
	        var atmosVersion:String =  d..cosNs::Version.cosNs::Atmos.toString();
	        service.atmosVersion = atmosVersion;
	        
	        return service;
		}
		
		/**
		 * Processes the replica info response
		 */
		private function processGetLsoResponse( request:EsuRestRequest ):Object {
			// Parse return headers.  Regular metadata is in x-emc-meta and
			// listable metadata is in x-emc-listable-meta
			//var meta:MetadataList = new MetadataList();
			//readMetadata( meta, request.getReponseHeaders()[ "x-emc-meta" ], false );           
			//readMetadata( meta, request.getReponseHeaders()[ "x-emc-listable-meta" ], true );
			var rawHeaders:String = request.getRawHeaders();
			logger.debug( "ReplicaInfo: raw headers: " + encodeURIComponent(rawHeaders) );
			
			var responseData:ByteArray = request.getReponseData();
			if( responseData == null ) {
				logger.debug( "ResponseData is null" );
			} else {
				responseData.position = 0;
				logger.debug( "Response data length: " + responseData.length );
				logger.debug( "Response: " + responseData.readUTFBytes( responseData.length ) );
			}
			
			// Find start & end of XML package
			var lines:Array = rawHeaders.split("\n");
			var found:Boolean = false;
			var xml:String = "";
			
			for each(var line:String in lines) {
				if( !found ) {
					if( line.indexOf( "<?xml" ) != -1 ) {
						// found start
						found = true;
						// parse out beginning
						xml = line.substr( line.indexOf( "<?xml" ) ) + "\n";
					}
				} else {
					xml += line + "\n";
					
					if( line.indexOf( "</maui:Lso" ) != -1 || line.indexOf( "</maui: Lso" ) != -1 ) {
						// done
						break;
					}
				}
			}
			
			
			return xml;
		}


	}
}